Final Year Project

This document provides a summary and allows the work to be examined.

All the imports have been collated for convience in the first cell.

In [1]:
import math
import test
import numpy as np
import pandas as pd
from Utility import *
from DataType import *
import track_vortices
import holoviews as hv
import plotly.io as pio
from holoviews import opts
import tracking_techniques
import extraction_techniques
from pandas import DataFrame
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.figure_factory as ff
from mpl_toolkits.mplot3d import Axes3D
from holoviews.streams import Stream, param

0. Preparing the data

Data is in the form of '.dat' files. These are read, reformatted and saved to prevent constant reformatting.

This can be done manually, or as a whole folder - which takes longer.

In [ ]:
import readData

readData(folder = True)

1. Vortex Identification

Data for this section will initally use the first sample only, load it.

In [10]:
frame_0001 = loadData(parent='data', filename='Cam1_0001.data_v2')

1.1 Initial data as a quiver plot

Running the code below produces a quiver representation of a single frame

In [11]:
# Create quiver figure
fig = ff.create_quiver(frame_0001.getAll('x'), frame_0001.getAll('y'), frame_0001.getAll('u'), frame_0001.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)
fig.update_layout(
    title_text='Initial Data as a quiver plot',
    showlegend=False
)

fig.show()

1.2 Identification

Running the identification algorithm produces the following results

In [12]:
vortcies, all_nodes = extraction_techniques.Jiang(frame_0001, growth_step=False)

# Create quiver figure
fig = ff.create_quiver(frame_0001.getAll('x'), frame_0001.getAll('y'), frame_0001.getAll('u'), frame_0001.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)

for vortexKey in vortcies:
    vortex = vortcies[vortexKey]
    keys = list(vortex.children.nodes.keys())
    keys.append(tuple(vortexKey))               
    fig.add_trace(go.Scatter(x=[item[0] for item in keys], y=[item[1] for item in keys],
        mode='markers',
        marker_size=12,
        name='points'))
                
fig.update_layout(
    title_text='Initial Data as a quiver plot including extracted vortex cores',
    showlegend=False
)

fig.show()

1.3 Growth

Running the code below adds potential vortex information to the plot after growing from the core regions

In [14]:
vortcies, all_nodes = extraction_techniques.Jiang(frame_0001)

# Create quiver figure
fig = ff.create_quiver(frame_0001.getAll('x'), frame_0001.getAll('y'), frame_0001.getAll('u'), frame_0001.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)

for vortexKey in vortcies:
    vortex = vortcies[vortexKey]
    keys = list(vortex.children.nodes.keys())
    keys.append(tuple(vortexKey))               
    fig.add_trace(go.Scatter(x=[item[0] for item in keys], y=[item[1] for item in keys],
        mode='markers',
        marker_size=12,
        name='points'))
                
fig.update_layout(
    title_text='Initial Data as a quiver plot including extracted vortcies',
    showlegend=False
)

fig.show()

1.4 Classification using KNN

After extracting a classification step is completed, to clean up the output. There are two variables that can be changed: nearest neighbours and threshold.

Nearest neighbours

The first cell preloads the quiver data from the second frame. Sample two has been choosen due to two points that are present, that are not in the first frame.

Plotply allows for 3D interactive plots, but is slower to render. For this reason the figure outputs are saved as a binary file to be loaded when replotting the frame's data.

The following cells explore and test the parameters to get the appropriate output.

In [13]:
frame_0002 = loadData(parent='data', filename='Cam1_0002.data_v2')
vortcies_0002, all_nodes_0002 = extraction_techniques.Jiang(frame_0002)
In [ ]:
# Create, then save the quiver figure
base_fig = ff.create_quiver(frame_0002.getAll('x'), frame_0002.getAll('y'), frame_0002.getAll('u'), frame_0002.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)

filehandler = open('Cam1_0002.plotly_quiver', 'wb')
pickle.dump(base_fig, filehandler)
In [12]:
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, track = False).show()

test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=5).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=6).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=7).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=8).show()

The NN values tested from 5 as starting from either 1 or 2 causes the velocity magnitude, used for weighting the distance, to equal zero throwing an error.

As the nearest neighbours increase the central vortex at (-100, 20) gets classified as one vortex. However, the two points mentioned before (-25, 80) & (-25, 100) have been classified incorrectly as part as the nearest cluster - resulting in the need for a threshold.

The distance threshold

The threshold has been added to validate a node as a valid neighbour - before it is used in its classification.

In [15]:
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=5, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=5, threshold=15).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=8, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=8, threshold=15).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=12, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0002.plotly_quiver'), vortcies_0002, all_nodes_0002, n=13, threshold=10).show()

The parameters that worked for the second frame (nn = 8, threshold = 10) were applied to other frames to check if the results seemed correct. Frames 6 and 18 have been highlighted here, due to different behaviour shown.

In [20]:
data_0006 = loadData(parent='data', filename='Cam1_0006.data_v2')
vortcies_0006, all_nodes_0006 = extraction_techniques.Jiang(data_0006)

data_0018 = loadData(parent='data', filename='Cam1_0018.data_v2')
vortcies_0018, all_nodes_0018 = extraction_techniques.Jiang(data_0018)
In [ ]:
fig_0006 = ff.create_quiver(data_0006.getAll('x'), data_0006.getAll('y'), data_0006.getAll('u'), data_0006.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)

filehandler = open('Cam1_0006.plotly_quiver', 'wb')
pickle.dump(fig_0006, filehandler)
In [17]:
test.draw_fig(loadData('data', 'Cam1_0006.plotly_quiver'), vortcies_0006, all_nodes_0006, n=8, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0006.plotly_quiver'), vortcies_0006, all_nodes_0006, n=12, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0006.plotly_quiver'), vortcies_0006, all_nodes_0006, n=13, threshold=10).show()
In [ ]:
fig_0018 = ff.create_quiver(data_0018.getAll('x'), data_0018.getAll('y'), data_0018.getAll('u'), data_0018.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)

filehandler = open('Cam1_0018.plotly_quiver', 'wb')
pickle.dump(fig_0018, filehandler)
In [21]:
test.draw_fig(loadData('data', 'Cam1_0018.plotly_quiver'), vortcies_0018, all_nodes_0018, n=8, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0018.plotly_quiver'), vortcies_0018, all_nodes_0018, n=12, threshold=10).show()
test.draw_fig(loadData('data', 'Cam1_0018.plotly_quiver'), vortcies_0018, all_nodes_0018, n=13, threshold=10).show()

Examining the results shows that the values (nn = 13, threshold = 10) work best for these edge cases, as shown below for the first frame.

In [23]:
vortcies, all_nodes = extraction_techniques.Jiang(frame_0001)
vortcies, families = tracking_techniques.preprocess_data(vortcies, all_nodes.nodes)

# Create quiver figure
fig = ff.create_quiver(frame_0001.getAll('x'), frame_0001.getAll('y'), frame_0001.getAll('u'), frame_0001.getAll('v'),
                       scale=.25,
                       arrow_scale=.4,
                       name='quiver',
                       line_width=1)

for vortexKey in vortcies:
    vortex = vortcies[vortexKey]
    keys = list(vortex.children.nodes.keys())
    keys.append(tuple(vortexKey))               
    fig.add_trace(go.Scatter(x=[item[0] for item in keys], y=[item[1] for item in keys],
        mode='markers',
        marker_size=12,
        name='points'))
                
fig.update_layout(
    title_text='Initial Data as a quiver plot including extracted vortcies, now after classification',
    showlegend=False
)

fig.show()

2. Vortex Tracking

The series of samples were analysed collectivly to produce produce paths. Three algorthims were looked at and two implemented in an attempt to solve the correspondance problem.

This will load all the features to use.

In [2]:
data = loadData(parent='data', filename='all.features')

Samtanety compared to Reinders

In [3]:
from tracking_techniques import Samtaney

def plot_tracked_Samantey(ax, matches):
    iMatch = 0
    for plane in matches:
        for match in plane:
            x = np.linspace(match[0][0], match[1][0], 5)
            y = np.linspace(match[0][1], match[1][1], 5)
            z = np.linspace(iMatch, iMatch+1, 5)

            ax.plot(x, z, y, c='red')

        iMatch += 1
        
def plot_tracked_Reiner(ax, paths):
    for path in paths:
        path = paths[path]
        x = [v.x for v in path.vortices]
        y = [v.y for v in path.vortices]
        z = list(range(path.start, path.end))
        
        ax.plot(x, z, y, c='red')

fig = plt.figure()
ax1 = fig.add_subplot(1, 2, 1, projection='3d')
ax2 = fig.add_subplot(1, 2, 2, projection='3d')
plot_tracked_Samantey(ax1, Samtaney(data))
paths = track_vortices.tracker(data).found_paths
plot_tracked_Reiner(ax2, paths)

Reinder's

The following uses Reinder's algorithm. The first plot shows vortex cores and pathlines, and the second only shows pathlines.

In [4]:
import track_vortices 

def plot_3D_scatter_familygroupings(data):
    sample_number = 0
    for sample in data:
        first = True

        for s in sample:
            z_values = []
            sizes = []
            for i in range(0, len(sample[s].nodes['x'])):
                z_values.append(sample_number)
                sizes.append(1)
            fig.add_trace(
                go.Scatter3d(
                    x=sample[s].nodes['x'],
                    y=z_values,
                    z=sample[s].nodes['y'],
                    mode="markers",
                    hovertext=sizes
                ))
        sample_number += 1
        
def plot_tracked_Reiner(paths):
    for path in paths:
        path = paths[path]
        x_values = [v.x for v in path.vortices]
        y_values = [v.y for v in path.vortices]
        z_values = list(range(path.start, path.end))
        
        fig.add_trace(
            go.Scatter3d(
                x=x_values,
                y=z_values,
                z=y_values,
                mode="lines",
                marker_size=8,
            ))

fig = go.Figure()
data = loadData(parent='data', filename='all.features')
plot_3D_scatter_familygroupings(data)
paths = track_vortices.tracker(data, min_path_len=0, thresholds = [10, 2, 2, 1],  weights = [1, 1, 0.7, 0.3]).found_paths
plot_tracked_Reiner(paths)

fig.update_layout(
    title_text='Correspondance',
    height=1000,
    width=1000,
    showlegend=False
)

fig.layout.scene.xaxis.title = "X"
fig.layout.scene.yaxis.title = "Time Steps"
fig.layout.scene.zaxis.title = "Y"

fig.show()
In [7]:
def plot_tracked_Reiner(paths):
    for path in paths:
        x_values = [v.x for v in path.vortices]
        y_values = [v.y for v in path.vortices]
        z_values = list(range(path.start, path.end))
        
        fig.add_trace(
            go.Scatter3d(
                x=x_values,
                y=z_values,
                z=y_values,
                mode="lines",
                hovertext=path.confidence
            ))
        
data = loadData(parent='data', filename='all.features')
fig = go.Figure()
plot_tracked_Reiner(track_vortices.tracker(data, min_path_len = 0).found_paths_dbg)
fig.update_layout(
    title_text='Paths',
    height=1000,
    width=1000,
    showlegend=False
)
fig.show()

3. Visualisation

3.1 Interactivity

The library used thus far, Plotly, doesn't allow for variable input. HoloViews has been used instead which also allows interaction via HTML.

In [4]:
hv.extension('bokeh')
hv.extension('matplotlib')

def plot_3D_scatter_familygroupings(data, sample_number=0):
    while True:
        sample = data[sample_number]
        x = []
        y = []
        colour = []
        family_number = 0
        for family in sample:
            x.extend(sample[family].nodes['x'])
            y.extend(sample[family].nodes['y'])
            for i in range(0, sample[family].size):
                colour.append(family_number)
            family_number += 1

        coords = list(zip(x, y))
        scatter = hv.Scatter(coords)
        yield scatter
        sample_number +=1

# Generate data
data = loadData(parent='data', filename='all.features')

# adding surfaces to subplots.
generator = plot_3D_scatter_familygroupings(data)
dmap = hv.DynamicMap(generator, streams=[Stream.define('Next')()])
hv.HoloMap({i:next(dmap) for i in range(10)}, kdims='Iteration')
Out[4]:

3.2 Annimation

A simple extention from iteracting with the plot is annimation. Unfortunatly, the HoloViews doesn't seem to be working, code has still been provided.

In [5]:
# Ten frame annimation of the extracted vortcies
# http://holoviews.org/user_guide/Responding_to_Events.html

# Method A
for iMap in range(10):
    dmap.event()

# Method B
dmap.periodic(0.1, 1000, timeout=3)

3.3 3D Object

Access to paths and edge coordinates in each frame gives the possiblity of generating a volume and using a marching cube algorithm to produce an object who's depth represents the change in time. Edge coordinates have been obtained by using a convex hull algorithm on 'families'.

3.31 Convex Hull Algoriths

Gift wrapping
Graham scan
Monotone Chain
Scipy.spatial
In [ ]:
import convex_hull_algos
from scipy.spatial import ConvexHull

border = convex_hull_algos.gift_wrapping(node)
border = convex_hull_algos.graham_scan(node)
border = convex_hull_algos.monotone_chain(node)
border = ConvexHull(nodes)